package com.electronic.signer;

import com.electronic.appearance.PdfSignatureAppearance;
import com.electronic.exception.ResultCode;
import com.electronic.exception.ServiceException;
import com.itextpdf.commons.utils.FileUtil;
import com.itextpdf.forms.PdfAcroForm;
import com.itextpdf.forms.PdfSigFieldLock;
import com.itextpdf.forms.fields.PdfFormField;
import com.itextpdf.forms.fields.PdfSignatureFormField;
import com.itextpdf.kernel.exceptions.PdfException;
import com.itextpdf.kernel.geom.Rectangle;
import com.itextpdf.kernel.pdf.*;
import com.itextpdf.kernel.pdf.annot.PdfWidgetAnnotation;
import com.itextpdf.signatures.IExternalSignatureContainer;
import com.itextpdf.signatures.PdfSignature;
import com.itextpdf.signatures.SignatureUtil;
import lombok.Setter;

import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.security.GeneralSecurityException;
import java.util.*;

/**
 * @Author: IFCA
 * @Description: PdfExtendsSigner 类，扩展了 com.itextpdf.signatures.PdfSigner 类，用于处理 PDF 签名相关操作
 */
public class PdfExtendsSigner extends com.itextpdf.signatures.PdfSigner {

    /**
     * 是否只签名一次的标志，默认为 false
     */
    @Setter
    private boolean sigOnce = false;

    /**
     * 用于存储只签名一次的字段名称的 HashSet
     */
    private final HashSet<String> sigOnceFieldNames = new HashSet<>();

    /**
     * 构造函数，初始化 PdfSigner 对象
     *
     * @param reader       PDF 读取器
     * @param outputStream 输出流
     * @param properties   盖章属性
     * @throws IOException 可能抛出的输入输出异常
     */
    public PdfExtendsSigner(PdfReader reader, OutputStream outputStream, StampingProperties properties) throws IOException {
        super(reader, outputStream, properties);
    }

    /**
     * 将外观添加到文档中
     *
     * @param appearance 签名外观
     * @param fieldName  字段名称
     * @throws IOException 可能抛出的输入输出异常
     */
    public void addAppearanceToDocument(PdfSignatureAppearance appearance, String fieldName) throws IOException {
        // 处理页面矩形
        Rectangle pageRect = appearance.getPageRect();

        // 先创建 PDF 签名表单字段
        PdfSignatureFormField sigField = PdfFormField.createSignature(this.document);
        sigField.setFieldName(fieldName);
        sigField.put(PdfName.V, (new PdfString("")).setHexWriting(true));

        // 创建 PDF 小部件注释
        PdfWidgetAnnotation widget = new PdfWidgetAnnotation(pageRect);
        widget.setFlags(PdfFormField.VISIBLE);

        // 设置小部件的页面
        int pageNumber = appearance.getPageNumber();
        widget.setPage(this.document.getPage(pageNumber));

        // 获取或创建小部件的外观字典
        PdfDictionary ap = widget.getAppearanceDictionary();
        if (Objects.isNull(ap)) {
            ap = new PdfDictionary();
            widget.put(PdfName.AP, ap);
        }

        // 设置外观字典的值
        ap.put(PdfName.N, appearance.getAppearance().getPdfObject());

        // 将小部件添加到签名表单字段
        sigField.addKid(widget);

        // 处理字段锁
        if (Objects.nonNull(this.fieldLock)) {
            this.fieldLock.getPdfObject().makeIndirect(this.document);
            sigField.put(PdfName.Lock, this.fieldLock.getPdfObject());
        }

        // 获取 PDF 表单并添加字段
        PdfAcroForm acroForm = PdfAcroForm.getAcroForm(document, true);
        acroForm.addField(sigField, this.document.getPage(pageNumber));

        // 添加字段名到集合
        this.sigOnceFieldNames.add(fieldName);
    }

    /**
     * 设置签名外观
     *
     * @param appearance 签名外观
     */
    public void setAppearance(PdfSignatureAppearance appearance) {
        this.appearance = appearance;
    }

    /**
     * 外部容器签名方法
     *
     * @param externalSignatureContainer 外部签名容器
     * @param estimatedSize              预留空间大小
     * @throws GeneralSecurityException 可能抛出的通用安全异常
     * @throws IOException              可能抛出的输入输出异常
     */
    @Override
    public void signExternalContainer(IExternalSignatureContainer externalSignatureContainer, int estimatedSize) throws GeneralSecurityException, IOException {
        if (this.closed) {
            throw new PdfException("This instance of PdfSigner has been already closed.");
        }

        PdfSignature dic = new PdfSignature();
        dic.setReason(appearance.getReason());
        dic.setLocation(appearance.getLocation());
        dic.setSignatureCreator(appearance.getSignatureCreator());
        dic.setContact(appearance.getContact());
        dic.setDate(new PdfDate(this.getSignDate()));

        externalSignatureContainer.modifySigningDictionary(dic.getPdfObject());
        this.cryptoDictionary = dic;
        Map<PdfName, Integer> exc = new HashMap<>();
        exc.put(PdfName.Contents, estimatedSize * 2 + 2);
        this.preClose(exc);
        PdfDictionary dic2 = new PdfDictionary();
        InputStream data = this.getRangeStream();

        try {
            byte[] encodedSig = externalSignatureContainer.sign(data);
            if (estimatedSize < encodedSig.length) {
//                throw new IOException("Not enough space estimatedSize=" + estimatedSize + ",acturalSize=" + encodedSig.length);
                throw new ServiceException(ResultCode.ERROR_SIGNATURE_PHOTO,ResultCode.ERROR_SIGNATURE_PHOTO.getMessage());
            }

            byte[] paddedSig = new byte[estimatedSize];
            System.arraycopy(encodedSig, 0, paddedSig, 0, encodedSig.length);
            dic2.put(PdfName.Contents, (new PdfString(paddedSig)).setHexWriting(true));
            this.close(dic2);
            this.closed = true;
        } catch (IOException | GeneralSecurityException e) {
            throw e;
        } catch (Exception e) {
            throw new GeneralSecurityException(e);
        }
    }

    /**
     * 预关闭方法
     *
     * @param exclusionSizes map
     * @throws IOException 可能抛出的输入输出异常
     */
    @Override
    protected void preClose(Map<PdfName, Integer> exclusionSizes) throws IOException {
        if (this.preClosed) {
            throw new PdfException("Document has been already pre closed.");
        }

        if (Objects.isNull(this.cryptoDictionary)) {
            throw new PdfException("No crypto dictionary defined.");
        }

        if (this.appearance instanceof PdfSignatureAppearance) {
            ((PdfSignatureAppearance) appearance).setSignDate(this.signDate);
        }

        this.preClosed = true;
        PdfAcroForm acroForm = PdfAcroForm.getAcroForm(document, true);
        SignatureUtil sgnUtil = new SignatureUtil(document);
        String name = getFieldName();

        boolean fieldExist = sgnUtil.doesSignatureFieldExist(name);
        acroForm.setSignatureFlags(3);
        PdfSigFieldLock fieldLock = null;

        this.cryptoDictionary.getPdfObject().makeIndirect(this.document);
        if (fieldExist) {
            fieldLock = populateExistingField(acroForm);
        }

        this.exclusionLocations = new HashMap<>();
        PdfLiteral lit = new PdfLiteral(80);
        this.exclusionLocations.put(PdfName.ByteRange, lit);
        this.cryptoDictionary.put(PdfName.ByteRange, lit);

        for (Map.Entry<PdfName, Integer> pdfNameIntegerEntry : exclusionSizes.entrySet()) {
            Map.Entry<PdfName, Integer> entry = (Map.Entry) pdfNameIntegerEntry;
            PdfName key = entry.getKey();
            lit = new PdfLiteral(entry.getValue());
            this.exclusionLocations.put(key, lit);
            this.cryptoDictionary.put(key, lit);
        }

        if (this.certificationLevel > 0) {
            this.addDocMDP(this.cryptoDictionary);
        }

        if (fieldLock != null) {
            this.addFieldMDP(this.cryptoDictionary, fieldLock);
        }

        if (this.signatureEvent != null) {
            this.signatureEvent.getSignatureDictionary(this.cryptoDictionary);
        }

        this.setSameSignature();
        if (this.certificationLevel > 0) {
            PdfDictionary docmdp = new PdfDictionary();
            docmdp.put(PdfName.DocMDP, this.cryptoDictionary.getPdfObject());
            this.document.getCatalog().put(PdfName.Perms, docmdp);
            this.document.getCatalog().setModified();
        }

        if ((acroForm.getPdfObject()).isIndirect()) {
            acroForm.setModified();
        } else {
            this.document.getCatalog().setModified();
        }

        (this.cryptoDictionary.getPdfObject()).flush(false);
        this.document.close();
        this.range = new long[this.exclusionLocations.size() * 2];
        long byteRangePosition = this.exclusionLocations.get(PdfName.ByteRange).getPosition();
        this.exclusionLocations.remove(PdfName.ByteRange);
        int idx = 1;

        long n;
        PdfLiteral lit1;
        for (Iterator<PdfLiteral> var40 = this.exclusionLocations.values().iterator(); var40.hasNext(); this.range[idx++] = (long) lit1.getBytesCount() + n) {
            lit1 = var40.next();
            n = lit1.getPosition();
            this.range[idx++] = n;
        }

        Arrays.sort(this.range, 1, this.range.length - 1);

        for (int k = 3; k < this.range.length - 2; k += 2) {
            long[] var10000 = this.range;
            var10000[k] -= this.range[k - 1];
        }

        if (Objects.isNull(this.tempFile)) {
            this.bout = this.temporaryOS.toByteArray();
            this.range[this.range.length - 1] = (long) this.bout.length - this.range[this.range.length - 2];
            ByteArrayOutputStream bos = new ByteArrayOutputStream();
            PdfOutputStream os = new PdfOutputStream(bos);
            os.write(91);

            for (long l : this.range) {
                (os.writeLong(l)).write(32);
            }

            os.write(93);
            System.arraycopy(bos.toByteArray(), 0, this.bout, (int) byteRangePosition, bos.size());
            os.close();
        } else {
            try {
                this.raf = FileUtil.getRandomAccessFile(this.tempFile);
                long len = this.raf.length();
                this.range[this.range.length - 1] = len - this.range[this.range.length - 2];
                ByteArrayOutputStream bos = new ByteArrayOutputStream();
                PdfOutputStream os = new PdfOutputStream(bos);
                os.write(91);

                for (long l : this.range) {
                    os.writeLong(l).write(32);
                }

                os.write(93);
                this.raf.seek(byteRangePosition);
                this.raf.write(bos.toByteArray(), 0, bos.size());
                os.close();
            } catch (IOException var26) {
                try {
                    this.raf.close();
                } catch (Exception ignored) {
                }

                try {
                    this.tempFile.delete();
                } catch (Exception ignored) {
                }

                throw var26;
            }
        }
    }

    /**
     * 用于填充现有签名表单字段的方法
     *
     * @param acroForm PDF 表单对象
     * @return PdfSigFieldLock 对象，表示签名字段的锁定信息
     * @throws IOException 可能在操作过程中抛出的输入输出异常
     */
    protected PdfSigFieldLock populateExistingField(PdfAcroForm acroForm) throws IOException {
        // 获取指定名称的 PDF 签名表单字段
        PdfSignatureFormField sigField = (PdfSignatureFormField) acroForm.getField(this.fieldName);
        // 设置签名表单字段的值
        sigField.put(PdfName.V, this.cryptoDictionary.getPdfObject());
        // 获取签名表单字段的锁定字典
        PdfSigFieldLock fieldLock = sigField.getSigFieldLockDictionary();
        // 如果锁定字典为空且本地的锁定不为空
        if (Objects.isNull(fieldLock) && Objects.nonNull(this.fieldLock)) {
            // 将本地锁定的 PDF 对象变为间接引用
            this.fieldLock.getPdfObject().makeIndirect(this.document);
            // 设置签名表单字段的锁定字典
            sigField.put(PdfName.Lock, this.fieldLock.getPdfObject());
            // 将本地锁定赋值给 fieldLock
            fieldLock = this.fieldLock;
        }

        // 设置签名表单字段的页面信息
        sigField.put(PdfName.P, this.document.getPage(this.appearance.getPageNumber()).getPdfObject());
        PdfDictionary ap;

        // 获取签名表单字段的标志对象
        PdfObject obj = sigField.getPdfObject().get(PdfName.F);
        int flags = 0;
        // 如果标志对象不为空且为数字
        if (Objects.nonNull(obj) && obj.isNumber()) {
            // 获取标志值
            flags = ((PdfNumber) obj).intValue();
        }
        // 设置新的标志值
        flags |= 128;
        // 更新签名表单字段的标志
        sigField.put(PdfName.F, new PdfNumber(flags));
        // 创建新的外观字典
        ap = new PdfDictionary();
        // 如果外观是特定类型
        if (this.appearance instanceof PdfSignatureAppearance) {
            // 设置外观字典的相关值
            ap.put(PdfName.N, ((PdfSignatureAppearance) appearance).getAppearance().getPdfObject());
        }
        // 设置签名表单字段的外观字典
        sigField.put(PdfName.AP, ap);
        // 标记签名表单字段已修改
        sigField.setModified();
        // 返回签名字段的锁定信息
        return fieldLock;
    }

    private void setSameSignature() {
        if (!this.sigOnce) {
            return;
        }

        // 获取文档的 PDF 表单
        PdfAcroForm acroForm = PdfAcroForm.getAcroForm(document, true);
        // 获取表单字段的迭代器
        Iterator<Map.Entry<String, PdfFormField>> formFieldsIte = acroForm.getFormFields().entrySet().iterator();

        // 创建一个列表用于存储表单字段的条目
        List<Map.Entry<String, PdfFormField>> entries = new ArrayList<>();
        // 将迭代器中的条目添加到列表中
        while (formFieldsIte.hasNext()) {
            entries.add(formFieldsIte.next());
        }

        // 遍历表单字段的条目列表
        for (Map.Entry<String, PdfFormField> entry : entries) {
            // 获取当前条目的表单字段
            PdfFormField field = entry.getValue();
            // 获取表单字段的 PDF 对象字典
            PdfDictionary fieldDict = field.getPdfObject();
            // 获取字段类型
            PdfName fieldFT = fieldDict.getAsName(PdfName.FT);

            // 如果字段类型不是按钮或签名，则跳过当前循环
            if (!(PdfName.Btn.equals(fieldFT) || PdfName.Sig.equals(fieldFT))) {
                continue;
            }

            // 获取表单字段的文本字符串
            PdfString tString = fieldDict.getAsString(PdfName.T);
            // 将文本字符串转换为 Unicode 字符串
            String tValue = tString.toUnicodeString();
            // 如果指定的名称集合不包含当前字段的值，则跳过当前循环
            if (!this.sigOnceFieldNames.contains(tValue)) {
                continue;
            }

            // 获取表单字段的值字典
            PdfDictionary value = fieldDict.getAsDictionary(PdfName.V);
            // 如果值字典为空
            if (Objects.isNull(value)) {
                // 设置表单字段的值
                fieldDict.put(PdfName.V, (this.cryptoDictionary.getPdfObject()).getIndirectReference());
            }
        }
    }
}
